#!/usr/bin/php
<?php

// Algo:
//
// + 1. Connect to Asterisk
// + 2. Parse the abaxd.conf
// + 3. Get extensions state
// + 4. Set initial led state
// + 5. Start threads for each xml update
// + 6. Wait for device state changes
// + 7. Send led state every update_period minutes or on device state change

require_once("AsteriskManager.php");
require_once("Thread.php");

// Command line options:
//
// --help
// -H ami_host [default localhost]
// -P ami_port [default 5038]
// -u AMI user [default abaxd]
// -p AMI pass [default abaxd_pass]

$ami_host = "localhost";
$ami_port = "5038";
$ami_user = "abaxd";
$ami_pass = "abaxd_pass";

$opts = "h";
$opts .= "H:";
$opts .= "P:";
$opts .= "u:";
$opts .= "p:";

$args = getopt($opts);

function usage()
{
 echo "\nabaxd usage:\n\n";
 echo "\t-h\t\t- usage/short help\n";
 echo "\t-H [ami host]\t- Asterisk AMI host [localhost]\n";
 echo "\t-P [ami port]\t- Asterisk AMI port [5038]\n";
 echo "\t-u [ami user]\t- Asterisk AMI user [abaxd]\n";
 echo "\t-p [ami pass]\t- Asterisk AMI pass [abaxd_pass]\n";
 echo "\n";
}

foreach (array_keys($args) as $arg) {
  switch ($arg) {
    case 'H':
      $ami_host = $args['H'];
      break;
    case 'P':
      $ami_port = $args['P'];
      break;
    case 'u':
      $ami_user = $args['u'];
      break;
    case 'p':
      $ami_pass = $args['p'];
      break;
    case 'h':
      usage();
      exit(1);
  }
}

function abaxd_log($message)
{
 echo $message."\n";
}

function get_category_values($conf, $cat)
{
 $values = array();
 // get categories count
 $cat_count = preg_match_all('/Category-/', $conf, $matches);
 if (!$cat_count) {
   echo "No categories found !\n";
   exit(-6);
 }
 $categories = explode("Category-", $conf, $cat_count + 1);
 foreach ($categories as $category) {
   $cats = explode(":", $category, 2);
   list($cat_name) = split("\r\n", trim($cats[1]));
   if (!strcmp($cat_name, $cat)) {
     // create array
     $value = strtok($category, ":\n");
     $is_val = 0;
     while ($value != false) {
       if ($is_val) {
        $val = explode("=", trim($value), 2);
        $values[$val[0]] = $val[1];
        $is_val = 0;
      }
       if (strstr($value, "Line-"))
         $is_val = 1;
       $value = strtok(":\n");
    }
   }
 }
   return $values;
}

// default general values
$debug_level = 1;
$debug_file = "/var/log/asterisk/abax.log";
$update_period = 1;
// default leds_map values
$leds_map = array("unreachable" => "slowflash", "not_inuse" => "off",
  "busy" => "on", "ringing" => "fastflash", "onhold" => "none");

// TODO: remove class - use simple leds array
class Watching_Phone {
  // array of leds for this phone
  public $leds;
};

class Phone {
  // phone ip
  public $ip;
  // state - 0 off, 1 - reacheable
  public $state;
};

// key phone name, data Phone class
$phones_list = array();

// $watched_extensions is array where key is watching extension and data is
// array of objects that have key as the waching phone name and data type
// is array of leds
$watched_extensions = array();

function add_extension_to_watch_list($extension, $phone_name, $led_number)
{
  global $watched_extensions;

  if (isset($watched_extensions[$extension])) {
    // there will be two cases:
    // 1: adding led to exisiting phone
    // 2: adding new watch phone
    if (isset($watched_extensions[$extension][$phone_name])) // adding led
      $watched_extensions[$extension][$phone_name]->leds[] = $led_number;
    else { // adding phone
      $new_phone = new Watching_Phone;
      $new_phone->leds = array();
      $new_phone->leds[] = $led_number;
      $watched_extensions[$extension][$phone_name] = $new_phone;
    }
  } else {
    $new_phone = new Watching_Phone;
    $new_phone->leds = array();
    $new_phone->leds[] = $led_number;
    $watched_extensions[$extension][$phone_name] = $new_phone;
  }
}

function parse_config($response)
{
 global $abaxd;
 global $debug_level, $debug_file, $update_period, $leds_map, $phones_list;

 $abaxd_config = $response;
 // general section
 $general_values = get_category_values($abaxd_config, "general");
 if (!count($general_values)) {
   echo "Can't find general category !\n";
   exit(-3);
 }
  $debug_level = $general_values['debug_level'];
  $debug_file = $general_values['debug_file'];
  $update_period = $general_values['update_period'];
 // leds_map section
 $leds_map_values = get_category_values($abaxd_config, "leds_map");
 if (!count($leds_map_values))
   echo "Can't find leds_map category, using default values.\n";
 else
   $leds_map = $leds_map_values;
 // phones_list section
 $phones_list_values = get_category_values($abaxd_config, "phones_list");
 if (!count($phones_list_values)) {
   echo "Can't find phones_list category !\n";
   exit(-4);
 }
 abaxd_log("Processing ".count($phones_list_values)." phones config :");
 $phone_count = 1;
 foreach ($phones_list_values as $phone_section => $phone_name) {
   // get leds config
   $phones_leds_values = get_category_values($abaxd_config, $phone_section);
   if (!count($phones_leds_values)) {
     abaxd_log("Can't find section '".$phone_section."' !");
     exit(-5);
  }
  $leds = array();
  foreach ($phones_leds_values as $led => $extension) {
    if (strstr($led, "led")) {
      $led_numbers = explode("led", $led);
      $led_number = $led_numbers[1];
      $leds[$led_number] = $extension;
      add_extension_to_watch_list($extension, $phone_name, $led_number);
   } else if (strstr($led, "blf_filename")) {
     try {
       $file_response = $abaxd->GetConfig($extension);
       $file_config = $file_response;
       $phones_leds_values = get_category_values($file_config, $phone_section);
       if (!count($phones_leds_values)) {
         abaxd_log("Can't find section '".$phone_section."' in file ".$extension." !");
         exit(-8);
      }
       foreach ($phones_leds_values as $led => $extension) {
         if (strstr($led, "led")) {
           $led_numbers = explode("led", $led);
           $led_number = $led_numbers[1];
           $leds[$led_number] = $extension;
           add_extension_to_watch_list($extension, $phone_name, $led_number);
         } else if (strstr($led, "blf_filename")) {
           abaxd_log("No blf_filename allowed here, only led !");
           exit(-9);
         }
       }
     } catch (PEAR_Exception $e) {
       abaxd_log("Error processing filename ".$extension." for section [".$phone_section."] !");
       abaxd_log("Reason:\n\n".$e."\n");
       exit(-10);
     }
   }
  } // end leds section parse
   abaxd_log(" ".$phone_count.". "."section [".$phone_section."], name '".$phone_name."', total ".count($leds)." leds");
   $phones_list[$phone_name] = new Phone;
   $phone_count++;
 } // end phone section parse
}

// gets phones ip's and their reachability state
function update_phone_ip_and_state()
{
 global $abaxd, $phones_list;

  $response = $abaxd->getSipPeers();
  $var = explode("ListItems:", $response);
  $total_phones = trim($var[1]);
  $phones = explode("Event: PeerEntry", $response, $total_phones + 1);
  for ($i = 1; $i < ($total_phones + 1); $i++) {
    $var = explode("ObjectName:", $phones[$i]);
    list($phone_name) = split("\r\n", trim($var[1]));
    $var = explode("IPaddress:", $phones[$i]);
    list($phone_ip) = split("\r\n", trim($var[1]));
    $var = explode("Status:", $phones[$i]);
    list($phone_state) = split("\r\n", trim($var[1]));
    if (strstr($phone_state, "OK")) {
      if (isset($phones_list[$phone_name])) {
        $phones_list[$phone_name]->ip = $phone_ip;
        $phones_list[$phone_name]->state = 1;
     }
   }
 }
}

function asterisk_state2abaxd_state($ast_state)
{
 switch ($ast_state) {
   case "INVALID":
   case "UNKNOWN":
   case "UNAVAILABLE":
     return "unreachable";
   case "NOT_INUSE":
   case "Not in use":
     return "not_inuse";
   case "BUSY":
   case "Busy":
   case "INUSE":
     return "busy";
   case "RINGINUSE":
   case "Ringing":
   case "RINGING":
     return "ringing";
   case "HOLDINUSE":
   case "ONHOLD":
     return "onhold";
   default:
     return "unknown";
 }
}

// returns unreachable / not_inuse / busy / ringing / onhold
function get_device_state($device)
{
 global $abaxd;

 $response = $abaxd->getDeviceState($device);
 $state_data = explode("DeviceName: ".$device."\r\nDeviceState: ", $response, 2);
 return asterisk_state2abaxd_state(trim($state_data[1]));
}

function send_xml($phone, $xml_data)
{
 global $xml_push_source_ip;
  $xml = "xml=".$xml_data;
  $post = "POST / HTTP/1.1\r\n";
  $post .= "Host: $phone\r\n";
  $post .= "Connection: Close\r\n";
  $post .= "Content-Type: text/xml; charset=ISO-8859-1\r\n";
  $post .= "Content-Length: ".strlen($xml)."\r\n\r\n";
  $fp = @fsockopen($phone, 80, $errno, $errstr, 5);
  if ($fp) {
    @fputs($fp, $post.$xml);
    flush();
    fclose($fp);
  }
}

$leds_cache = array();

// updates leds on phone

function set_leds($ip, $leds, $led_state)
{
 global $leds_cache;

 $xml = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n";
 $xml .= "<AastraIPPhoneExecute Beep=\"no\">\n";
 $leds_needed_update = 0;
 foreach ($leds as $led) {
   // check if cached
   if (isset($leds_cache[$ip])) {
     if (isset($leds_cache[$ip][$led])) {
       if ($leds_cache[$ip][$led] == $led_state)
         continue;
     }
   }
   // add to cache
   $leds_cache[$ip][$led] = $led_state;
   $leds_needed_update++;
   // abaxd_log("setting led".$led." on ".$ip." to ".$led_state);
   // each M670i expmod has 36 keys
   $expmod = intval($led/36) + 1;
   $key = intval($led - intval(($expmod - 1) * 36));
   $xml .= " <ExecuteItem URI=\"Led: expmod".$expmod." key".$key."=".$led_state."\"/>\n";
 }
  $xml .= "</AastraIPPhoneExecute>\n";
  if ($leds_needed_update) {
    $thr = new Thread('send_xml');
    $thr->start($ip, $xml);
  }
}

function set_phones_leds($wphones, $new_state)
{
 global $phones_list, $leds_map;

  if (isset($leds_map[$new_state]))
    $led_state = $leds_map[$new_state];
  else // unknown state
    return;
  foreach ($wphones as $name => $watch_phone) {
    // update the phones leds
    $is_reacheable = $phones_list[$name]->state;
    if ($is_reacheable) {
      $ip = $phones_list[$name]->ip;
      $leds = $watch_phone->leds;
      set_leds($ip, $leds, $led_state);
   }
  }
}

function update_extensions_state()
{
 global $watched_extensions;

 foreach ($watched_extensions as $extension => $wphones) {
    $state = get_device_state($extension);
    // abaxd_log("Extension ".$extension." has ".$state." state, ".count($wphones)." watching phones");
    set_phones_leds($wphones, $state);
  } 
}

function do_unconditional_leds_update()
{
 abaxd_log("Doing forced leds update");
 update_phone_ip_and_state();
 update_extensions_state();
}

function process_device_change_state($change_state_event)
{
 global $watched_extensions;

 $device_data = explode("DeviceName: ", $change_state_event);
 list($extension) = split("\r\n", trim($device_data[1]));
 // is it watched extension ?
 if (isset($watched_extensions[$extension])) {
   $state_data = explode("DeviceState: ", $change_state_event);
   list($state) = split("\r\n", trim($state_data[1]));
   $led_state = asterisk_state2abaxd_state($state);
   $wphones = $watched_extensions[$extension];
   set_phones_leds($wphones, $led_state);
 }
}

if (!Thread::available()) {
   die('Threads not supported');
}

echo "Connecting to Asterisk at ".$ami_host.":".$ami_port." as AMI user ".$ami_user." ... ";

$params = array('server' => $ami_host, 'port' => $ami_port);
$abaxd = new Net_AsteriskManager($params);

try {
  $abaxd->connect();
  $abaxd->login($ami_user, $ami_pass);
} catch (PEAR_Exception $e) {
   echo "failed\n";
   echo "Reason:\n\n".$e."\n\n";
   exit(-1);
}

echo "Ok\nGetting abaxd.conf ... ";

try {
  $response = $abaxd->GetConfig("abaxd.conf");
} catch (PEAR_Exception $e) {
  echo "Reason:\n\n".$e."\n\n";
  exit(-2);
}

echo "Ok\n";

parse_config($response);

abaxd_log("Total ".count($watched_extensions)." extension will be watched");

do_unconditional_leds_update();

abaxd_log("Waiting for events");

$start_time = time();

while (1) {
  $response = $abaxd->GetDeviceStateChangedEvent(1);
  if ($response != false)
    process_device_change_state($response);
  $current_time = time();
  if ((($current_time - $start_time)/60) >= $update_period) {
    do_unconditional_leds_update();
    $start_time = $current_time;
 }
}

?>
